<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover"><title>MallChat项目笔记 | Jixer的小屋</title><meta name="author" content="Jixer"><meta name="copyright" content="Jixer"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="#ffffff"><meta name="description" content="微信扫码登录准备工具 误区：微信扫码登录每个人都可以，不是只有认证号才行。测试阶段可以用测试接口来进行测试，只不过项目部署后才需要认证使用 注意：微信回调接口需要开启内网穿透   微信公众号的开发者账号：点击此处进行注册 内网穿透工具：点击此处注册  引入依赖12345&lt;dependency&gt;    &lt;groupId&gt;com.github.binarywang&lt;&#x2F;gr">
<meta property="og:type" content="article">
<meta property="og:title" content="MallChat项目笔记">
<meta property="og:url" content="http://www.lijunxi.site/posts/2967897711/index.html">
<meta property="og:site_name" content="Jixer的小屋">
<meta property="og:description" content="微信扫码登录准备工具 误区：微信扫码登录每个人都可以，不是只有认证号才行。测试阶段可以用测试接口来进行测试，只不过项目部署后才需要认证使用 注意：微信回调接口需要开启内网穿透   微信公众号的开发者账号：点击此处进行注册 内网穿透工具：点击此处注册  引入依赖12345&lt;dependency&gt;    &lt;groupId&gt;com.github.binarywang&lt;&#x2F;gr">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://q1.qlogo.cn/g?b=qq&nk=2770063826&s=640">
<meta property="article:published_time" content="2024-02-08T11:19:12.000Z">
<meta property="article:modified_time" content="2024-05-07T03:10:23.261Z">
<meta property="article:author" content="Jixer">
<meta property="article:tag" content="SpringBoot">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://q1.qlogo.cn/g?b=qq&nk=2770063826&s=640"><link rel="shortcut icon" href="/img/logo/favicon.ico"><link rel="canonical" href="http://www.lijunxi.site/posts/2967897711/index.html"><link rel="preconnect"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><link rel="stylesheet" href="/css/index.css?v=4.13.0"><link rel="stylesheet" href="/pluginsSrc/@fortawesome/fontawesome-free/css/all.min.css?v=6.5.1"><link rel="stylesheet" href="/pluginsSrc/@fancyapps/ui/dist/fancybox/fancybox.css?v=5.0.33" media="print" onload="this.media='all'"><script>const GLOBAL_CONFIG = {
  root: '/',
  algolia: undefined,
  localSearch: {"path":"/search.xml","preload":true,"top_n_per_article":1,"unescape":false,"languages":{"hits_empty":"找不到您查询的内容：${query}","hits_stats":"共找到 ${hits} 篇文章"}},
  translate: undefined,
  noticeOutdate: undefined,
  highlight: {"plugin":"highlight.js","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":false},
  copy: {
    success: '复制成功',
    error: '复制错误',
    noSupport: '浏览器不支持'
  },
  relativeDate: {
    homepage: false,
    post: false
  },
  runtime: '',
  dateSuffix: {
    just: '刚刚',
    min: '分钟前',
    hour: '小时前',
    day: '天前',
    month: '个月前'
  },
  copyright: undefined,
  lightbox: 'fancybox',
  Snackbar: undefined,
  infinitegrid: {
    js: '/pluginsSrc/@egjs/infinitegrid/dist/infinitegrid.min.js?v=4.11.1',
    buttonText: '加载更多'
  },
  isPhotoFigcaption: false,
  islazyload: false,
  isAnchor: false,
  percent: {
    toc: true,
    rightside: false,
  },
  autoDarkmode: false
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
  title: 'MallChat项目笔记',
  isPost: true,
  isHome: false,
  isHighlightShrink: false,
  isToc: true,
  postUpdate: '2024-05-07 11:10:23'
}</script><script>(win=>{
      win.saveToLocal = {
        set: (key, value, ttl) => {
          if (ttl === 0) return
          const now = Date.now()
          const expiry = now + ttl * 86400000
          const item = {
            value,
            expiry
          }
          localStorage.setItem(key, JSON.stringify(item))
        },
      
        get: key => {
          const itemStr = localStorage.getItem(key)
      
          if (!itemStr) {
            return undefined
          }
          const item = JSON.parse(itemStr)
          const now = Date.now()
      
          if (now > item.expiry) {
            localStorage.removeItem(key)
            return undefined
          }
          return item.value
        }
      }
    
      win.getScript = (url, attr = {}) => new Promise((resolve, reject) => {
        const script = document.createElement('script')
        script.src = url
        script.async = true
        script.onerror = reject
        script.onload = script.onreadystatechange = function() {
          const loadState = this.readyState
          if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
          script.onload = script.onreadystatechange = null
          resolve()
        }

        Object.keys(attr).forEach(key => {
          script.setAttribute(key, attr[key])
        })

        document.head.appendChild(script)
      })
    
      win.getCSS = (url, id = false) => new Promise((resolve, reject) => {
        const link = document.createElement('link')
        link.rel = 'stylesheet'
        link.href = url
        if (id) link.id = id
        link.onerror = reject
        link.onload = link.onreadystatechange = function() {
          const loadState = this.readyState
          if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
          link.onload = link.onreadystatechange = null
          resolve()
        }
        document.head.appendChild(link)
      })
    
      win.activateDarkMode = () => {
        document.documentElement.setAttribute('data-theme', 'dark')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
        }
      }
      win.activateLightMode = () => {
        document.documentElement.setAttribute('data-theme', 'light')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#ffffff')
        }
      }
      const t = saveToLocal.get('theme')
    
        if (t === 'dark') activateDarkMode()
        else if (t === 'light') activateLightMode()
      
      const asideStatus = saveToLocal.get('aside-status')
      if (asideStatus !== undefined) {
        if (asideStatus === 'hide') {
          document.documentElement.classList.add('hide-aside')
        } else {
          document.documentElement.classList.remove('hide-aside')
        }
      }
    
      const detectApple = () => {
        if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
          document.documentElement.classList.add('apple')
        }
      }
      detectApple()
    })(window)</script><link rel="stylesheet" href="/css/custom-all-min.css"><link rel="stylesheet" href="/css/custom-fancybox-min.css"><link rel="stylesheet" href="/css/custom-share-min.css"><meta name="generator" content="Hexo 6.3.0"></head><body><div id="loading-box"><div class="loading-left-bg"></div><div class="loading-right-bg"></div><div class="spinner-box"><div class="configure-border-1"><div class="configure-core"></div></div><div class="configure-border-2"><div class="configure-core"></div></div><div class="loading-word">加载中...</div></div></div><script>(()=>{
  const $loadingBox = document.getElementById('loading-box')
  const $body = document.body
  const preloader = {
    endLoading: () => {
      $body.style.overflow = ''
      $loadingBox.classList.add('loaded')
    },
    initLoading: () => {
      $body.style.overflow = 'hidden'
      $loadingBox.classList.remove('loaded')
    }
  }

  preloader.initLoading()
  window.addEventListener('load',() => { preloader.endLoading() })

  if (false) {
    document.addEventListener('pjax:send', () => { preloader.initLoading() })
    document.addEventListener('pjax:complete', () => { preloader.endLoading() })
  }
})()</script><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src="" data-original="https://q1.qlogo.cn/g?b=qq&amp;nk=2770063826&amp;s=640" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="sidebar-site-data site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">52</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">19</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">7</div></a></div><hr class="custom-hr"/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 主页</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fa fa-graduation-cap"></i><span> 文章</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/categories/"><i class="fa-fw fa fa-archive"></i><span> 分类</span></a></li><li><a class="site-page child" href="/tags/"><i class="fa-fw fa fa-tags"></i><span> 标签</span></a></li><li><a class="site-page child" href="/archives/"><i class="fa-fw fa fa-folder-open"></i><span> 归档</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/links/"><i class="fa-fw fa fa-link"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div></div></div><div class="post" id="body-wrap"><header class="post-bg" id="page-header"><nav id="nav"><span id="blog-info"><a href="/" title="Jixer的小屋"><span class="site-name">Jixer的小屋</span></a></span><div id="menus"><div id="search-button"><a class="site-page social-icon search" href="javascript:void(0);"><i class="fas fa-search fa-fw"></i><span> 搜索</span></a></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 主页</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fa fa-graduation-cap"></i><span> 文章</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/categories/"><i class="fa-fw fa fa-archive"></i><span> 分类</span></a></li><li><a class="site-page child" href="/tags/"><i class="fa-fw fa fa-tags"></i><span> 标签</span></a></li><li><a class="site-page child" href="/archives/"><i class="fa-fw fa fa-folder-open"></i><span> 归档</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/links/"><i class="fa-fw fa fa-link"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div><div id="toggle-menu"><a class="site-page" href="javascript:void(0);"><i class="fas fa-bars fa-fw"></i></a></div></div></nav><div id="post-info"><h1 class="post-title">MallChat项目笔记</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">发表于</span><time class="post-meta-date-created" datetime="2024-02-08T11:19:12.000Z" title="发表于 2024-02-08 19:19:12">2024-02-08</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">更新于</span><time class="post-meta-date-updated" datetime="2024-05-07T03:10:23.261Z" title="更新于 2024-05-07 11:10:23">2024-05-07</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/%E5%AE%9E%E6%88%98%E9%A1%B9%E7%9B%AE/">实战项目</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-wordcount"><i class="far fa-file-word fa-fw post-meta-icon"></i><span class="post-meta-label">字数总计:</span><span class="word-count">9.3k</span><span class="post-meta-separator">|</span><i class="far fa-clock fa-fw post-meta-icon"></i><span class="post-meta-label">阅读时长:</span><span>36分钟</span></span><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title="MallChat项目笔记"><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">阅读量:</span><span id="busuanzi_value_page_pv"><i class="fa-solid fa-spinner fa-spin"></i></span></span></div></div></div></header><main class="layout" id="content-inner"><div id="post"><article class="post-content" id="article-container"><h2 id="微信扫码登录"><a href="#微信扫码登录" class="headerlink" title="微信扫码登录"></a>微信扫码登录</h2><h3 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h3><h4 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h4><blockquote>
<p>误区：微信扫码登录每个人都可以，不是只有认证号才行。测试阶段可以用测试接口来进行测试，只不过项目部署后才需要认证使用</p>
<p>注意：微信回调接口需要开启内网穿透</p>
</blockquote>
<ul>
<li>微信公众号的开发者账号：<a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/">点击此处进行注册</a></li>
<li>内网穿透工具：<a target="_blank" rel="noopener" href="https://natapp.cn/">点击此处注册</a></li>
</ul>
<h4 id="引入依赖"><a href="#引入依赖" class="headerlink" title="引入依赖"></a>引入依赖</h4><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.github.binarywang<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>weixin-java-mp<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>4.4.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h4 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h4><p>这里配置文件参考Binary Wang的代码</p>
<figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">wx:</span></span><br><span class="line">  <span class="attr">mp:</span></span><br><span class="line">    <span class="attr">callback:</span> <span class="string">$&#123;mallchat.wx.callback&#125;</span> <span class="comment"># 回调地址</span></span><br><span class="line">    <span class="attr">configs:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">appId:</span> <span class="string">$&#123;mallchat.wx.appId&#125;</span> <span class="comment"># 第一个公众号的appid</span></span><br><span class="line">        <span class="attr">secret:</span> <span class="string">$&#123;mallchat.wx.secret&#125;</span> <span class="comment"># 公众号的appsecret</span></span><br><span class="line">        <span class="attr">token:</span> <span class="string">$&#123;mallchat.wx.token&#125;</span> <span class="comment"># 接口配置里的Token值</span></span><br><span class="line">        <span class="attr">aesKey:</span> <span class="string">$&#123;mallchat.wx.aesKey&#125;</span> <span class="comment"># 接口配置里的EncodingAESKey值</span></span><br></pre></td></tr></table></figure>

<p>WxMpProperties.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;wx.mp&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WxMpProperties</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 是否使用redis存储access token</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> useRedis;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * redis 配置</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> RedisConfig redisConfig;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Data</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">RedisConfig</span> &#123;</span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * redis服务器 主机地址</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String host;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * redis服务器 端口号</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> Integer port;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * redis服务器 密码</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String password;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * redis 服务连接超时时间</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> Integer timeout;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 多个公众号配置信息</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;MpConfig&gt; configs;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Data</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">MpConfig</span> &#123;</span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 设置微信公众号的appid</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String appId;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 设置微信公众号的app secret</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String secret;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 设置微信公众号的token</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String token;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 设置微信公众号的EncodingAESKey</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String aesKey;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">toString</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> JSONUtil.toJsonStr(<span class="built_in">this</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>WxMpConfiguration.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableConfigurationProperties(WxMpProperties.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WxMpConfiguration</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> LogHandler logHandler;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> MsgHandler msgHandler;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> SubscribeHandler subscribeHandler;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ScanHandler scanHandler;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> WxMpProperties properties;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> WxMpService <span class="title function_">wxMpService</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 代码里 getConfigs()处报错的同学，请注意仔细阅读项目说明，你的IDE需要引入lombok插件！！！！</span></span><br><span class="line">        <span class="keyword">final</span> List&lt;WxMpProperties.MpConfig&gt; configs = <span class="built_in">this</span>.properties.getConfigs();</span><br><span class="line">        <span class="keyword">if</span> (configs == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;大哥，拜托先看下项目首页的说明（readme文件），添加下相关配置，注意别配错了！&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="type">WxMpService</span> <span class="variable">service</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">WxMpServiceImpl</span>();</span><br><span class="line">        service.setMultiConfigStorages(configs</span><br><span class="line">                .stream().map(a -&gt; &#123;</span><br><span class="line">                    WxMpDefaultConfigImpl configStorage;</span><br><span class="line">                    configStorage = <span class="keyword">new</span> <span class="title class_">WxMpDefaultConfigImpl</span>();</span><br><span class="line"></span><br><span class="line">                    configStorage.setAppId(a.getAppId());</span><br><span class="line">                    configStorage.setSecret(a.getSecret());</span><br><span class="line">                    configStorage.setToken(a.getToken());</span><br><span class="line">                    configStorage.setAesKey(a.getAesKey());</span><br><span class="line">                    <span class="keyword">return</span> configStorage;</span><br><span class="line">                &#125;).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -&gt; a, (o, n) -&gt; o)));</span><br><span class="line">        <span class="keyword">return</span> service;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> WxMpMessageRouter <span class="title function_">messageRouter</span><span class="params">(WxMpService wxMpService)</span> &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">WxMpMessageRouter</span> <span class="variable">newRouter</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">WxMpMessageRouter</span>(wxMpService);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 记录所有事件的日志 （异步执行）</span></span><br><span class="line">        newRouter.rule().handler(<span class="built_in">this</span>.logHandler).next();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 关注事件</span></span><br><span class="line">        newRouter.rule().async(<span class="literal">false</span>).msgType(EVENT).event(SUBSCRIBE).handler(<span class="built_in">this</span>.subscribeHandler).end();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 扫码事件</span></span><br><span class="line">        newRouter.rule().async(<span class="literal">false</span>).msgType(EVENT).event(EventType.SCAN).handler(<span class="built_in">this</span>.scanHandler).end();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 默认</span></span><br><span class="line">        newRouter.rule().async(<span class="literal">false</span>).handler(<span class="built_in">this</span>.msgHandler).end();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> newRouter;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="配置消息处理器"><a href="#配置消息处理器" class="headerlink" title="配置消息处理器"></a>配置消息处理器</h4><p>AbstractHandler.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">AbstractHandler</span> <span class="keyword">implements</span> <span class="title class_">WxMpMessageHandler</span> &#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="type">Logger</span> <span class="variable">logger</span> <span class="operator">=</span> LoggerFactory.getLogger(getClass());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>LogHandler.java（日志）</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LogHandler</span> <span class="keyword">extends</span> <span class="title class_">AbstractHandler</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> WxMpXmlOutMessage <span class="title function_">handle</span><span class="params">(WxMpXmlMessage wxMessage,</span></span><br><span class="line"><span class="params">                                    Map&lt;String, Object&gt; context, WxMpService wxMpService,</span></span><br><span class="line"><span class="params">                                    WxSessionManager sessionManager)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;\n接收到请求消息，内容：&#123;&#125;&quot;</span>, JSONUtil.toJsonStr(wxMessage));</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>MsgHandler.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MsgHandler</span> <span class="keyword">extends</span> <span class="title class_">AbstractHandler</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> WxMpXmlOutMessage <span class="title function_">handle</span><span class="params">(WxMpXmlMessage wxMessage,</span></span><br><span class="line"><span class="params">                                    Map&lt;String, Object&gt; context, WxMpService weixinService,</span></span><br><span class="line"><span class="params">                                    WxSessionManager sessionManager)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>ScanHandler.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ScanHandler</span> <span class="keyword">extends</span> <span class="title class_">AbstractHandler</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> WxMsgService wxMsgService;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 处理用户扫码【不是第一次】</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> WxMpXmlOutMessage <span class="title function_">handle</span><span class="params">(WxMpXmlMessage wxMpXmlMessage, Map&lt;String, Object&gt; map,</span></span><br><span class="line"><span class="params">                                    WxMpService wxMpService, WxSessionManager wxSessionManager)</span> <span class="keyword">throws</span> WxErrorException &#123;</span><br><span class="line">        <span class="comment">// 扫码事件处理</span></span><br><span class="line">        <span class="keyword">return</span> wxMsgService.scan(wxMpService, wxMpXmlMessage);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>SubscribeHandler.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SubscribeHandler</span> <span class="keyword">extends</span> <span class="title class_">AbstractHandler</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> WxMsgService wxMsgService;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 扫描登录二维码后第一次进入需要关注公众号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> wxMessage</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> context</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> weixinService</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> sessionManager</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> WxErrorException</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> WxMpXmlOutMessage <span class="title function_">handle</span><span class="params">(WxMpXmlMessage wxMessage,</span></span><br><span class="line"><span class="params">                                    Map&lt;String, Object&gt; context, WxMpService weixinService,</span></span><br><span class="line"><span class="params">                                    WxSessionManager sessionManager)</span> <span class="keyword">throws</span> WxErrorException &#123;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">this</span>.logger.info(<span class="string">&quot;新关注用户 OPENID: &quot;</span> + wxMessage.getFromUser());</span><br><span class="line"></span><br><span class="line">        <span class="type">WxMpXmlOutMessage</span> <span class="variable">responseResult</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            responseResult = <span class="built_in">this</span>.handleSpecial(weixinService, wxMessage);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="built_in">this</span>.logger.error(e.getMessage(), e);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (responseResult != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> responseResult;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">TextBuilder</span>().build(<span class="string">&quot;感谢关注&quot;</span>, wxMessage, weixinService);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="built_in">this</span>.logger.error(e.getMessage(), e);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 处理特殊请求，比如如果是扫码进来的，可以做相应处理</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> WxMpXmlOutMessage <span class="title function_">handleSpecial</span><span class="params">(WxMpService weixinService, WxMpXmlMessage wxMessage)</span></span><br><span class="line">            <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">return</span> wxMsgService.scan(weixinService, wxMessage);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="执行过程"><a href="#执行过程" class="headerlink" title="执行过程"></a>执行过程</h3><h4 id="1、获取登录二维码"><a href="#1、获取登录二维码" class="headerlink" title="1、获取登录二维码"></a>1、获取登录二维码</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleLoginReq</span><span class="params">(Channel channel)</span> &#123;</span><br><span class="line">  <span class="comment">// 生成登录随机码</span></span><br><span class="line">  <span class="type">Integer</span> <span class="variable">code</span> <span class="operator">=</span> generateLoginCode(channel);</span><br><span class="line">  <span class="comment">// 微信生成带参二维码</span></span><br><span class="line">  <span class="type">WxMpQrCodeTicket</span> <span class="variable">wxMpQrCodeTicket</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">      wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (<span class="type">int</span>) DURATION.getSeconds());</span><br><span class="line">  &#125; <span class="keyword">catch</span> (WxErrorException e) &#123;</span><br><span class="line">      e.printStackTrace();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 把二维码推送前端</span></span><br><span class="line">  sendMsg(channel, WebSocketAdapter.buildResp(wxMpQrCodeTicket));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这里我是对二维码做了过期策略，<code>wxMpQrCodeTicket.getUrl()</code>就是带参二维码的地址，通过草料二维码生成器便能生成一个二维码</p>
<h4 id="2、公众号返回登录授权"><a href="#2、公众号返回登录授权" class="headerlink" title="2、公众号返回登录授权"></a>2、公众号返回登录授权</h4><p>在<code>ScanHandler</code>类的<code>handle</code>方法中写入，专门处理扫码登录事件</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">openid</span> <span class="operator">=</span> wxMpXmlMessage.getFromUser();</span><br><span class="line"><span class="type">Integer</span> <span class="variable">code</span> <span class="operator">=</span> getEventKey(wxMpXmlMessage);</span><br><span class="line"><span class="keyword">if</span> (ObjectUtil.isNull(code))&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 保存用户信息</span></span><br><span class="line"><span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userDao.getByOpenId(openid);</span><br><span class="line"><span class="type">boolean</span> <span class="variable">registered</span> <span class="operator">=</span> Objects.nonNull(user);</span><br><span class="line"><span class="type">boolean</span> <span class="variable">authorized</span> <span class="operator">=</span> registered &amp;&amp; StrUtil.isNotBlank(user.getName());</span><br><span class="line"><span class="keyword">if</span> (registered &amp;&amp; authorized)&#123;</span><br><span class="line">	<span class="comment">// 用户已经注册了，直接返回用户+token信息</span></span><br><span class="line">    webSocketService.scanLoginSuccess(code, user.getId());</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (!registered)&#123;</span><br><span class="line">    <span class="comment">// 未写入数据库</span></span><br><span class="line">    <span class="type">User</span> <span class="variable">insert</span> <span class="operator">=</span> UserAdapter.buildUserSave(openid);</span><br><span class="line">    userService.register(insert);</span><br><span class="line">&#125;</span><br><span class="line">WAIT_AUTHORIZE_MAP.put(openid, code);</span><br><span class="line"><span class="comment">// 通知前端正在等待认证</span></span><br><span class="line">webSocketService.waitAuthorize(code);</span><br><span class="line"><span class="comment">// 拼接URL，获取认证地址</span></span><br><span class="line"><span class="type">String</span> <span class="variable">authorizeUrl</span> <span class="operator">=</span> String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLDecoder.decode(callback + <span class="string">&quot;wx/portal/public/callBack&quot;</span>));</span><br><span class="line"><span class="keyword">return</span> TextBuilder.build(<span class="string">&quot;请点击登录：&lt;a href=\&quot;&quot;</span> + authorizeUrl + <span class="string">&quot;\&quot;&gt;登录&lt;/a&gt;&quot;</span>, wxMpXmlMessage, wxMpService);</span><br></pre></td></tr></table></figure>

<p>当用户扫码并关注成功后，会自动弹出消息</p>
<blockquote>
<p>请点击登录：<a href="">登录</a></p>
</blockquote>
<p>用户点击登录后，会跳转到<code>&quot;https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&amp;redirect_uri=%s&amp;response_type=code&amp;scope=snsapi_userinfo&amp;state=STATE#wechat_redirect&quot;</code>微信授权页面，需要传入appid和回调接口地址，授权成功后将带着code参数访问给定的回调接口</p>
<h4 id="3、微信调用给定的回调接口"><a href="#3、微信调用给定的回调接口" class="headerlink" title="3、微信调用给定的回调接口"></a>3、微信调用给定的回调接口</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/callBack&quot;)</span></span><br><span class="line"><span class="keyword">public</span> RedirectView <span class="title function_">callBack</span><span class="params">(<span class="meta">@RequestParam</span> String code)</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// 根据code获取token</span></span><br><span class="line">        <span class="type">WxOAuth2AccessToken</span> <span class="variable">accessToken</span> <span class="operator">=</span> wxMpService.getOAuth2Service().getAccessToken(code);</span><br><span class="line">        <span class="comment">// 根据token获取用户信息</span></span><br><span class="line">        <span class="type">WxOAuth2UserInfo</span> <span class="variable">userInfo</span> <span class="operator">=</span> wxMpService.getOAuth2Service().getUserInfo(accessToken, <span class="string">&quot;zh_CN&quot;</span>);</span><br><span class="line">        wxMsgService.authorize(userInfo);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;callBack error&quot;</span>, e);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">RedirectView</span> <span class="variable">redirectView</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedirectView</span>();</span><br><span class="line">    redirectView.setUrl(<span class="string">&quot;https://mp.weixin.qq.com/s/m1SRsBG96kLJW5mPe4AVGA&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> redirectView;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>通过回调传来的code获取token，通过token来获取微信用户信息【只有昵称和头像】</p>
<p><code> wxMsgService.authorize(userInfo)</code>将用户不全的信息【用户头像，昵称】进行更新，并生成token进行返回</p>
<p>就这样，前端就拿到了后端传来的token</p>
<h2 id="用户认证技术Token方案"><a href="#用户认证技术Token方案" class="headerlink" title="用户认证技术Token方案"></a>用户认证技术Token方案</h2><p>Token 的生成方式通常有以下几种：</p>
<ul>
<li><strong>随机字符串</strong>：可以使用一些随机数生成算法，如 <strong>UUID</strong>、<strong>Snowflake(雪花算法)</strong> 等来生成一个随机的字符串作为 Token。由于随机字符串本身就是随机分布的，因此具有很高的安全性。</li>
<li><strong>JWT</strong>（JSON Web Token）：JWT 是一种基于 JSON 格式的开放标准（RFC 7519），用于在多方之间安全地传输信息。它将用户身份信息和权限等相关信息编码成一个 JSON 对象，并通过数字签名或者加密等方式进行验证和保护。JWT 除了可以用于 Token 登录外，还可以用于 API 认证、单点登录等场景。</li>
<li><strong>SessionID</strong>。</li>
</ul>
<p>通常的Token在服务器端的实现方式有这几个：</p>
<ol>
<li>用SessionID实现Token的功能</li>
<li>使用Json Web Token (JWT)</li>
<li>中心化存储Token</li>
</ol>
<p>下面分析一下各个存储方式的优缺点。</p>
<h3 id="Cookie-Session-登录"><a href="#Cookie-Session-登录" class="headerlink" title="Cookie + Session 登录"></a>Cookie + Session 登录</h3><p>HTTP 是一种<strong>无状态的协议</strong>。</p>
<p>无状态是指协议对于事务处理没有记忆能力，服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后，服务器根据请求返回数据，但不会记录任何信息。</p>
<p>为了解决 HTTP 无状态的问题，出现了 Cookie。</p>
<p>Cookie 是服务器端发送给客户端的一段特殊信息，这些信息以文本的方式存放在客户端，客户端每次向服务器端发送请求时都会带上这些特殊信息。</p>
<p><img src="" data-original="https://s11.ax1x.com/2024/02/01/pFMooHx.png"></p>
<p><strong>认证流程：</strong></p>
<ol>
<li>前端输入账号密码，提交给后端</li>
<li>后端验证成功后，创建一个<code>Session</code>。<code>Session</code>是一种服务器端保存用户会话信息的机制，用于识别多次请求之间的逻辑关系。</li>
<li>后端将<code>Session ID</code>（通常是一个随机的字符串）返回给前端，并通过 <code>Cookie</code> 的方式将<code>Session ID</code>保存在浏览器中。这样就可以保证当用户再次发送请求时，后端可以通过该 <code>Session ID </code>来识别用户身份，并完成相关的操作。</li>
<li>在后续的请求中，浏览器会自动将保存的 <code>Cookie </code>信息发送到后端进行验证，如果<code> Session ID</code>有效，则返回相应的数据。如果 <code>Session ID </code>失效或者不存在，则需要重新登录获取新的 <code>Session ID</code>。</li>
<li>用户退出时，后端要删除对应的<code>Session</code>信息</li>
</ol>
<h4 id="Cookie的设置原理"><a href="#Cookie的设置原理" class="headerlink" title="Cookie的设置原理"></a>Cookie的设置原理</h4><p>cookie的简单之处，在于前端是无感知的，无需额外开发。这是http协议的约定，后端可以通过返回的报文，将cookie设置进网页，网页下次请求也会<strong>自动携带</strong>。<code>SetCookie</code> 命令</p>
<p><img src="" data-original="https://s11.ax1x.com/2024/02/01/pFMoHUK.png"></p>
<h4 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h4><ul>
<li>跨域问题：由于 Cookie 只能在同域名下共享，因此跨域访问时无法访问到对应的 Cookie 信息。这时，可能需要采用一些其他的跨域解决方案，如 JSONP、CORS 等。</li>
<li>扩展性问题：由于 Session 信息存储在服务器端，当系统扩展到多台服务器时，需要采用一些集中式的 Session 管理方案，否则会出现 Session 不一致或者丢失等问题。</li>
<li>一些移动设备和浏览器可能会禁用 Cookie 和 Session 机制，这会导致无法正常登录</li>
</ul>
<h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><ul>
<li>给服务器的<code>sessionID</code>其实就相当于是一个<code>token</code>，只不过前端是无感知的设置进<code>cookie</code>的，这种方案 通常适用于后台。</li>
<li>由于cookie的一些限制，这个token最好还是由前端<strong>主动保存</strong>比如保存到<strong>localStorage</strong>。登录的时候主动从<strong>请求头</strong>携带。</li>
<li>由于现在都是集群部署，token的关系保存，最好又是<strong>集中化管理</strong>，或者<strong>无状态化管理</strong></li>
</ul>
<h3 id="JWT实现token"><a href="#JWT实现token" class="headerlink" title="JWT实现token"></a>JWT实现token</h3><p>简单来说JWT就是通过可逆加密算法，生成一串包含用户、过期时间等关键信息的Token，每次请求服务器拿到这个Token解密出来就能得到用户信息，从而判断用户状态。</p>
<p><img src="" data-original="https://s11.ax1x.com/2024/02/01/pFMob4O.png"></p>
<h4 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h4><ol>
<li>JWT的最大特点是<strong>服务器不保存会话状态，</strong>无论哪个服务器<strong>解析</strong>出来的Token信息都一样，而且不需要做任何查询操作，省掉了数据库&#x2F;Redis的开销</li>
</ol>
<h4 id="缺点-1"><a href="#缺点-1" class="headerlink" title="缺点"></a>缺点</h4><ol>
<li>正式因为JWT的特点，使用期间不可能取消令牌或更改令牌的权限，一旦JWT签发，在有效期内将会一直有效。</li>
<li>无法主动更新Token的有效性，只要用户传回来的Token没有过期，服务器就会认为这个用户操作是有效的。比如一下这个场景：某用户被封禁，此时该用户所有操作都应该被禁止，但是由于之前发给用户的JWT Token还没有过期，服务器仍然认为该用户操作合法。有一个解决方案是维护一张JWT黑名单表，只有没在表上的用户的JWT是有效的。<strong>但是随之而来又有一个问题便是这个JWT黑名单表存在哪里。存在服务器，那么又要搞多服务器同步。存在关系数据库，那么查数据库效率又低。存在Redis，则又回到了Token丢失问题。</strong> </li>
<li>其实解析JWT Token也是消耗服务器CPU的</li>
</ol>
<h4 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h4><ol>
<li>由于jwt是无状态的，它一发布开始，就意味着固定了过期时间。我们没法对他做<strong>失效</strong>，没法实现<strong>续期</strong>，它的好处也是显而易见的。不需要任何一个中心化的地方去保存它，管理它，查询它，比对它。</li>
</ol>
<h3 id="双token方案"><a href="#双token方案" class="headerlink" title="双token方案"></a>双token方案</h3><p>双token是为了<strong>解决jwt的续期</strong>问题的。由于jwt一颁布，就意味着在指定时间内能够通行。</p>
<ol>
<li>如果给的有<strong>效期过长</strong>，风险是比较大的，服务器失去了掌控力。在这期间如果想让用户失效，或者是有人盗取了token。都可以胡作非为好久。</li>
<li>如果给的有<strong>效期过短</strong>，用户经常需要重新登录，体验也很不好。</li>
<li>如果<strong>中心化管理</strong>用户状态，也就是每次解析jwt token之后，还需要去中心化比对能否通过。这样又违背了初衷。增加每次<strong>认证的耗时</strong></li>
</ol>
<p>双token分为<code>access_token</code>和<code>refresh_token</code>。一般<code>access_token</code>的有效期可以设置为10分钟，<code>refresh_token</code>的有效期可以设置为7天。用户每次请求都用<code>access_token</code>，如果前端发现请求401，也就是过期了，就用<code>refresh_token</code>去重新申请一个<code>access_token</code>。继续请求。</p>
<p>这里的关键在于，<code>refresh_token</code>申请<code>access_token</code>的时候，用户是无感知的，前后端的框架自动去更新这个新的<code>access_token</code>。</p>
<p>还有一个点在申请<code>access_token</code>的时候，后端这时候会去校验用户的状态等问题，如果发现用户被禁用了，就申请不到token了。</p>
<h4 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h4><p>双token是一个多方平衡的完美方案。它希望对用户的认证有所<strong>掌控</strong>，又不希望每次的检验会增加<strong>耗时</strong>。它不想给用户<strong>过长的授权时间</strong>，又不想用户因此<strong>频繁登录</strong>影响体验。因此变成了每隔一段<code>access_token</code>的过期时间，都会重新掌控局面，进行重新认证的复杂判断。</p>
<h3 id="中心管理token"><a href="#中心管理token" class="headerlink" title="中心管理token"></a>中心管理token</h3><p>JWT碰巧有去中心化的特性，但为了能够控制它的<strong>上下线</strong>，<strong>主动下线</strong> ，<strong>登录续期</strong>等功能。我们依然可以对它进行中心化的管理。</p>
<p><del>这也是抹茶当前采用的方式(参考的大佬文章)</del>。依赖redis中心化管理uid-》token的信息。确保一个uid只有一个有效的token。用户登录后，每一次认证都会解析出uid，并请求redis进行token比对。并且异步判断有效期小于一天，进行续期。</p>
<blockquote>
<p>有人说为啥不用uuid做token呢，既然都是redis中心存储，用uuid还可以少一次解析。</p>
</blockquote>
<p>如果用uuid，前端每一次请求除了带上uuid还需要带上uid。</p>
<p>因为单纯用uuid，黑客很有可能不断遍历uuid去撞库，碰巧撞到有关联的在线用户。而如果将uuid和uid一起比对，哪怕uuid碰巧撞到了登录的用户，还需要确保是相同的uid。这个概率会降低非常之多。</p>
<p>用jwt的话，正好包含了uid，让前端传起来方便，所以就这么选择了。</p>
<p>大家明白了其中的差别，到时候就懂得怎么去对线面试官的。其实我们用jwt，但是却没怎么用到它的特性。本质上这样的场景用个uuid就差不多了</p>
<h2 id="分布式锁"><a href="#分布式锁" class="headerlink" title="分布式锁"></a>分布式锁</h2><p>Redisson是架设在Redis基础上的一个Java驻内存数据网格（In-Memory Data Grid）。充分的利用了Redis键值数据库提供的一系列优势，基于Java实用工具包中常用接口，为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力，大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务，更进一步简化了分布式环境中程序相互之间的协作。</p>
<p><strong>依赖</strong></p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.redisson<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>redisson-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>使用Redission实现分布式很简单</p>
<p>比如就一个简单的发放物品</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">&quot;acquireItem&quot;</span> + idempotent);</span><br><span class="line"><span class="type">boolean</span> <span class="variable">b</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line">AssertUtil.isTrue(b, <span class="string">&quot;请求太频繁了&quot;</span>);</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// 是否已经获取了</span></span><br><span class="line">    <span class="type">UserBackpack</span> <span class="variable">userBackpack</span> <span class="operator">=</span> userBackpackDao.getByIdempotent(idempotent);</span><br><span class="line">    <span class="keyword">if</span> (Objects.nonNull(userBackpack))&#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 发放物品</span></span><br><span class="line">    <span class="type">UserBackpack</span> <span class="variable">insert</span> <span class="operator">=</span> UserBackpackAdapter.buildUserBackpack(uid, itemId, idempotent);</span><br><span class="line">    userBackpackDao.save(insert);</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    lock.unlock();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>通过<code>redissonClient.getLock(key)</code>获取锁对象，用<code>lock.tryLock()</code>就能获取锁，用<code>lock.unlock()</code>就能释放锁</p>
<p>现在对这个redisson的锁的部分进行封装，分别使用使用<strong>编程式</strong>的方法和<strong>注解</strong>方法进行改造，简化逻辑代码</p>
<h3 id="编程式"><a href="#编程式" class="headerlink" title="编程式"></a>编程式</h3><p>新建一个<code>LockService</code>来实现分布式锁的公共部分</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LockService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RedissonClient redissonClient;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> waiteTime</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> timeUnit</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> supplier 只有出参没有入参, Function：既有出参又有入参</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;T&gt;</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@SneakyThrows</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; T <span class="title function_">executeWithLock</span><span class="params">(String key, <span class="type">int</span> waiteTime, TimeUnit timeUnit, Supplier&lt;T&gt; supplier)</span> &#123;</span><br><span class="line">        <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(key);</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">b</span> <span class="operator">=</span> lock.tryLock(waiteTime, timeUnit);</span><br><span class="line">        <span class="keyword">if</span> (!b)&#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BusinessException</span>(CommonErrorEnum.LOCK_LIMIT);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> supplier.get();</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            lock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; T <span class="title function_">executeWithLock</span><span class="params">(String key, Supplier&lt;T&gt; supplier)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> executeWithLock(key, -<span class="number">1</span>, TimeUnit.MILLISECONDS, supplier);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> runnable 既不传参，也不返参</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;T&gt;</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> InterruptedException</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; T <span class="title function_">executeWithLock</span><span class="params">(String key, Runnable runnable)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> executeWithLock(key, -<span class="number">1</span>, TimeUnit.MILLISECONDS, () -&gt; &#123;</span><br><span class="line">            runnable.run();</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这里对<code>executeWithLock</code>方法进行了重载，能够适应多种加锁的场景【可以实现有返回值的，无返回值，既有出参又有入参的】</p>
<p>说明一下<code>Supplier</code>：只有出参没有入参，<code>Function</code>：既有出参又有入参，<code>Runnable</code>：既不传参，也不返参</p>
<p><code>executeWithLock</code>方法实现了分布式的公共部分，具体的代码逻辑由<code>supplier.get()</code>实现，也就是通过参数传入，需要我们自己来写</p>
<p><strong>编程式的修改代码如下</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">lockService.executeWithLock(<span class="string">&quot;acquireItem&quot;</span> + idempotent, () -&gt; &#123;</span><br><span class="line">    <span class="comment">// 是否已经获取了</span></span><br><span class="line">    <span class="type">UserBackpack</span> <span class="variable">userBackpack</span> <span class="operator">=</span> userBackpackDao.getByIdempotent(idempotent);</span><br><span class="line">    <span class="keyword">if</span> (Objects.nonNull(userBackpack))&#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 发放物品</span></span><br><span class="line">    <span class="type">UserBackpack</span> <span class="variable">insert</span> <span class="operator">=</span> UserBackpackAdapter.buildUserBackpack(uid, itemId, idempotent);</span><br><span class="line">    userBackpackDao.save(insert);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>比原先相比代码量减少了，可复用性提高了</p>
<h3 id="注解"><a href="#注解" class="headerlink" title="注解"></a>注解</h3><p>新建一个分布式注解<code>RedissonLock</code></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Retention(RetentionPolicy.RUNTIME)</span> <span class="comment">// 运行时生效</span></span><br><span class="line"><span class="meta">@Target(ElementType.METHOD)</span> <span class="comment">// 作用在方法上</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> RedissonLock &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * key的前缀</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    String <span class="title function_">prefixKey</span><span class="params">()</span> <span class="keyword">default</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * EL表达式</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    String <span class="title function_">key</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 等待锁时间，默认不等待</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="type">int</span> <span class="title function_">waitTime</span><span class="params">()</span> <span class="keyword">default</span> -<span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 时间单位，默认毫秒</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    TimeUnit <span class="title function_">timeUnit</span><span class="params">()</span> <span class="keyword">default</span> TimeUnit.MILLISECONDS;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>对这个注解进行切面</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="meta">@Order(0)</span> <span class="comment">// 确保在事务注解之前先执行，分布式在事务外</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedissonLockAspect</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> LockService lockService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Around(&quot;@annotation(redissonLock)&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">around</span><span class="params">(ProceedingJoinPoint joinPoint, RedissonLock redissonLock)</span> &#123;</span><br><span class="line">        <span class="type">Method</span> <span class="variable">method</span> <span class="operator">=</span> ((MethodSignature) joinPoint.getSignature()).getMethod();</span><br><span class="line">        <span class="type">String</span> <span class="variable">prefix</span> <span class="operator">=</span> StrUtil.isBlank(redissonLock.prefixKey()) ? SpElUtils.getMethodKey(method) : redissonLock.prefixKey();</span><br><span class="line">        <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> SpElUtils.parseSpEl(method, joinPoint.getArgs(), redissonLock.key());</span><br><span class="line">        <span class="keyword">return</span> lockService.executeWithLock(prefix + <span class="string">&quot;:&quot;</span> + key, redissonLock.waitTime(), redissonLock.timeUnit(), joinPoint::proceed);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>prefix为前缀，若没有给定，就需要通过<code>method.getDeclaringClass() + &quot;#&quot; + method.getName()</code>为默认的前缀</p>
<p>key为关键字，需要通过给定的<strong>EL表达式</strong>来进行匹配【会将给定的key和方法的参数名称进行匹配，若一致就写入<code>EvaluationContext（类似于map，键为方法参数名称，值为方法参数的值）</code>中】，用<code>parser.parseExpression</code>解析EL表达式，返回解析结果</p>
<p>具体的匹配代码如下</p>
<p>SpElUtils：EL表达式工具</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpElUtils</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExpressionParser</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpelExpressionParser</span>();</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">DefaultParameterNameDiscoverer</span> <span class="variable">parameterNameDiscoverer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DefaultParameterNameDiscoverer</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">parseSpEl</span><span class="params">(Method method, Object[] args, String spEl)</span> &#123;</span><br><span class="line">        String[] params = Optional.ofNullable(parameterNameDiscoverer.getParameterNames(method)).orElse(<span class="keyword">new</span> <span class="title class_">String</span>[]&#123;&#125;);<span class="comment">//解析参数名</span></span><br><span class="line">        <span class="type">EvaluationContext</span> <span class="variable">context</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StandardEvaluationContext</span>();<span class="comment">//el解析需要的上下文对象</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; params.length; i++) &#123;</span><br><span class="line">            context.setVariable(params[i], args[i]);<span class="comment">//所有参数都作为原材料扔进去</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Expression</span> <span class="variable">expression</span> <span class="operator">=</span> parser.parseExpression(spEl);</span><br><span class="line">        <span class="keyword">return</span> expression.getValue(context, String.class);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">getMethodKey</span><span class="params">(Method method)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> method.getDeclaringClass() + <span class="string">&quot;#&quot;</span> + method.getName();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>注解切面的最后就是将解析出来的key，waitTime，timeUnit，joinPoint::proceed(自己写的逻辑)当作参数传入<code>lockService.executeWithLock()</code>自己写的编程式方法中</p>
<p><strong>使用注解方法修改的代码如下</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">acquireItem</span><span class="params">(Long uid, Long itemId, IdempotentEnum idempotentEnum, String businessId)</span> &#123;</span><br><span class="line">    <span class="comment">// 幂等号</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">idempotent</span> <span class="operator">=</span> getIdempotent(uid, itemId, businessId);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 分布式注解方法</span></span><br><span class="line">    userBackpackService.doAcquire(uid, itemId, idempotent);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="meta">@RedissonLock(key = &quot;#idempotent&quot;, waitTime = 5000)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doAcquire</span><span class="params">(Long uid, Long itemId, String idempotent)</span> &#123;</span><br><span class="line">    <span class="comment">// 是否已经获取了</span></span><br><span class="line">    <span class="type">UserBackpack</span> <span class="variable">userBackpack</span> <span class="operator">=</span> userBackpackDao.getByIdempotent(idempotent);</span><br><span class="line">    <span class="keyword">if</span> (Objects.nonNull(userBackpack)) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 发放物品</span></span><br><span class="line">    <span class="type">UserBackpack</span> <span class="variable">insert</span> <span class="operator">=</span> UserBackpackAdapter.buildUserBackpack(uid, itemId, idempotent);</span><br><span class="line">    userBackpackDao.save(insert);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="IP归属地"><a href="#IP归属地" class="headerlink" title="IP归属地"></a>IP归属地</h2><h3 id="具体流程"><a href="#具体流程" class="headerlink" title="具体流程"></a>具体流程</h3><p>IP归属地是社交网站常用的功能，实现起来非常简单</p>
<p>大致步骤：hutool工具类解析出ip地址 -&gt; 通过淘宝的接口获取归属地 -&gt; 更新数据库</p>
<p>本项目是在<strong>登录之后</strong>，<strong>带token请求http</strong>，<strong>带token连接ws</strong>，这几处进行ip解析</p>
<h3 id="核心代码"><a href="#核心代码" class="headerlink" title="核心代码"></a>核心代码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">loginSuccess</span><span class="params">(Channel channel, User user, String token)</span> &#123;</span><br><span class="line">    <span class="comment">// 保存用户channel和uid对应关系</span></span><br><span class="line">    <span class="type">WSChannelExtraDTO</span> <span class="variable">wsChannelExtraDTO</span> <span class="operator">=</span> ONLINE_WS_MAP.get(channel);</span><br><span class="line">    wsChannelExtraDTO.setUid(user.getId());</span><br><span class="line">    <span class="comment">// 给前端推送消息</span></span><br><span class="line">    sendMsg(channel, WebSocketAdapter.buildResp(user, token));</span><br><span class="line">    <span class="comment">// 用户上线事件</span></span><br><span class="line">    user.setLastOptTime(LocalDateTime.now());</span><br><span class="line">    user.refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));</span><br><span class="line">    applicationEventPublisher.publishEvent(<span class="keyword">new</span> <span class="title class_">UserOnlineEvent</span>(<span class="built_in">this</span>, user));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>User实体</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@EqualsAndHashCode(callSuper = false)</span></span><br><span class="line"><span class="meta">@TableName(value = &quot;user&quot;, autoResultMap = true)</span></span><br><span class="line"><span class="meta">@Builder</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">serialVersionUID</span> <span class="operator">=</span> <span class="number">1L</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户id</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">      <span class="meta">@TableId(value = &quot;id&quot;, type = IdType.AUTO)</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户昵称</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;name&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户头像</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;avatar&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String avatar;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 性别 1为男性，2为女性</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;sex&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> Integer sex;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 微信openid用户标识</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;open_id&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String openId;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 在线状态 1在线 2离线</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;active_status&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> Integer activeStatus;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 最后上下线时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;last_opt_time&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime lastOptTime;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * ip信息</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(value = &quot;ip_info&quot;, typeHandler = JacksonTypeHandler.class)</span></span><br><span class="line">    <span class="keyword">private</span> IpInfo ipInfo;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 佩戴的徽章id</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;item_id&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> Long itemId;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 使用状态 0.正常 1拉黑</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;status&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> Integer status;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 创建时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;create_time&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime createTime;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@TableField(&quot;update_time&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime updateTime;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">refreshIp</span><span class="params">(String attr)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (ipInfo == <span class="literal">null</span>)&#123;</span><br><span class="line">            ipInfo = <span class="keyword">new</span> <span class="title class_">IpInfo</span>();</span><br><span class="line">        &#125;</span><br><span class="line">        ipInfo.refreshIp(attr);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>IpInfo实体</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">IpInfo</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 注册的ip</span></span><br><span class="line">    <span class="keyword">private</span> String createIp;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 注册时ip的详情</span></span><br><span class="line">    <span class="keyword">private</span> IpDetail ipDetail;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 更新的ip</span></span><br><span class="line">    <span class="keyword">private</span> String updateIp;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 更新时的ip详情</span></span><br><span class="line">    <span class="keyword">private</span> IpDetail updateIpDetail;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">refreshIp</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(ip))&#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (StrUtil.isBlank(createIp))&#123;</span><br><span class="line">            createIp = ip;</span><br><span class="line">        &#125;</span><br><span class="line">        updateIp = ip;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">needRefreshIp</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">needRefresh</span> <span class="operator">=</span> Optional.ofNullable(updateIpDetail)</span><br><span class="line">                .map(IpDetail::getIp)</span><br><span class="line">                .filter(item -&gt; Objects.equals(updateIp, item))</span><br><span class="line">                .isPresent(); <span class="comment">// 判断结果是否是null</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> needRefresh ? <span class="literal">null</span> : updateIp;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">refreshIpDetail</span><span class="params">(IpDetail ipDetail)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (Objects.equals(ipDetail.getIp(), createIp))&#123;</span><br><span class="line">            <span class="built_in">this</span>.ipDetail = ipDetail;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (Objects.equals(ipDetail.getIp(), updateIp))&#123;</span><br><span class="line">            <span class="built_in">this</span>.updateIpDetail = ipDetail;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>refreshIp</code>是对当前登录的ip进行刷新，有个关键点就是先对注册ip进行判断，注册的ip无就需要对注册的ip进行赋值，其次在是更新的ip</p>
<p>在用户登录成功后，通过<code> applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));</code>发送异步消息SpringEvent，通过一个事件监听来接受</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserOnlineListener</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserDao userDao;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IpService ipService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Async</span> <span class="comment">// 异步执行</span></span><br><span class="line">    <span class="meta">@EventListener(classes = UserOnlineEvent.class)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">userOnline</span><span class="params">(UserOnlineEvent event)</span>&#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> event.getUser();</span><br><span class="line">        <span class="comment">// 更新用户信息【Ip，是否登录等】</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">update</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        update.setId(user.getId());</span><br><span class="line">        update.setLastOptTime(user.getLastOptTime());</span><br><span class="line">        update.setIpInfo(user.getIpInfo());</span><br><span class="line">        update.setActiveStatus(UserActiveStatusEnum.ONLINE.getType());</span><br><span class="line">        userDao.updateById(update);</span><br><span class="line">        <span class="comment">// 用户IP解析</span></span><br><span class="line">        ipService.refreshDetailAsync(user.getId());</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这里是对用户的信息做了更新【最后一次登录事件，登录状态，IP信息】，用<code>ipService.refreshDetailAsync(user.getId())</code>实现对IP的异步刷新解析</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">IpServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">IpService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="type">ExecutorService</span> <span class="variable">executor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ThreadPoolExecutor</span>(<span class="number">1</span>, <span class="number">1</span>,</span><br><span class="line">            <span class="number">0L</span>, TimeUnit.MILLISECONDS,</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">LinkedBlockingQueue</span>&lt;Runnable&gt;(<span class="number">500</span>), <span class="keyword">new</span> <span class="title class_">NamedThreadFactory</span>(<span class="string">&quot;refresh-ipDetail&quot;</span>, <span class="literal">false</span>));</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserDao userDao;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">refreshDetailAsync</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        executor.execute(() -&gt; &#123;</span><br><span class="line">            <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userDao.getById(id);</span><br><span class="line">            <span class="type">IpInfo</span> <span class="variable">ipInfo</span> <span class="operator">=</span> user.getIpInfo();</span><br><span class="line">            <span class="keyword">if</span> (Objects.isNull(ipInfo))&#123;</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 判断是否需要刷新ip详情</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> ipInfo.needRefreshIp();</span><br><span class="line">            <span class="keyword">if</span> (StrUtil.isBlank(ip))&#123;</span><br><span class="line">                <span class="comment">// 更新ip和更新ip一致无需刷新</span></span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 获取ip详情</span></span><br><span class="line">            <span class="type">IpDetail</span> <span class="variable">ipDetail</span> <span class="operator">=</span> tryGetIpDetailOrNull(ip);</span><br><span class="line">            <span class="keyword">if</span> (Objects.nonNull(ipDetail))&#123;</span><br><span class="line">                <span class="comment">// 刷新ipInfo的详情</span></span><br><span class="line">                ipInfo.refreshIpDetail(ipDetail);</span><br><span class="line">                <span class="comment">// 更新ip详情</span></span><br><span class="line">                <span class="type">User</span> <span class="variable">update</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">                update.setIpInfo(ipInfo);</span><br><span class="line">                update.setId(id);</span><br><span class="line">                userDao.updateById(update);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> IpDetail <span class="title function_">tryGetIpDetailOrNull</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">3</span>; i++)&#123;</span><br><span class="line">            <span class="type">IpDetail</span> <span class="variable">ipDetail</span> <span class="operator">=</span> getIpDetailOrNull(ip);</span><br><span class="line">            <span class="keyword">if</span> (Objects.nonNull(ipDetail))&#123;</span><br><span class="line">                <span class="keyword">return</span> ipDetail;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (InterruptedException e)&#123;</span><br><span class="line">                log.error(<span class="string">&quot;tryGetIpDetailOrNull InterruptedException&quot;</span>, e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> IpDetail <span class="title function_">getIpDetailOrNull</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">body</span> <span class="operator">=</span> HttpUtil.get(<span class="string">&quot;https://ip.taobao.com/outGetIpInfo?ip=&quot;</span> + ip + <span class="string">&quot;&amp;accessKey=alibaba-inc&quot;</span>);</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            ApiResult&lt;IpDetail&gt; result = JSONUtil.toBean(body, <span class="keyword">new</span> <span class="title class_">TypeReference</span>&lt;ApiResult&lt;IpDetail&gt;&gt;() &#123;</span><br><span class="line">            &#125;, <span class="literal">false</span>);</span><br><span class="line">            <span class="keyword">if</span> (result.isSuccess()) &#123;</span><br><span class="line">                <span class="keyword">return</span> result.getData();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在实现方法<code>refreshDetailAsync</code>中，使用了线程池来进行异步排队处理，设置了最大容量为1个，并且设置了<code>refresh-ipDetail</code>线程前缀来便于排查出问题</p>
<p>通过<code>needRefreshIp</code>判断是否需要刷新，若需要刷新就返回要刷新的ip地址，否则就返回null；是否刷新的判断依据是用用户当前数据库已经保存的IP解析的地址和当前登录的ip地址进行对比，若一致就返回null，否则就返回当前的登录的ip地址</p>
<p><code>tryGetIpDetailOrNull</code>方法是尝试获取ip解析信息，有3次机会，每次执行完有2秒的睡眠时间</p>
<p><code>refreshIpDetail</code>方法是刷新当前用户的ip解析信息，通过ip进行判断是注册ip还是更新ip，从而进行赋值</p>
<h2 id="黑名单功能"><a href="#黑名单功能" class="headerlink" title="黑名单功能"></a>黑名单功能</h2><h3 id="具体流程-1"><a href="#具体流程-1" class="headerlink" title="具体流程"></a>具体流程</h3><p>实现黑名单功能就是将用户的ip和id放在黑名单数据库中，通过拦截器判断登陆的用户是否在名单中，从而决定是否拦截通过</p>
<p>本项目中的黑名单功能需要用到权限，权限管理多用于b端项目，对于c端来说，权限没有到那么复杂的级别。因此权限设计只涉及到了用户身份</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1686979132303-1934c4b2-943f-45d3-b892-a78a228e4821.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_109,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10/resize,w_1280,limit_0"></p>
<p><strong>流程</strong>：登录的时候返回角色 -&gt; 管理拉黑【判断权限】-&gt; 推送给前端消息 -&gt; 写入黑名单表， 修改状态【SpringEvent事件异步处理】 -&gt; 拉黑用户拦截</p>
<h3 id="核心代码-1"><a href="#核心代码-1" class="headerlink" title="核心代码"></a><strong>核心代码</strong></h3><p>判断用户是否具有某个权限，用来set巧妙处理</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RoleDao</span> <span class="keyword">implements</span> <span class="title class_">IRoleService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserCache userCache;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">isPower</span><span class="params">(Long uid, RoleEnum roleEnum)</span> &#123;</span><br><span class="line">        <span class="comment">// 获取用户所有权限</span></span><br><span class="line">        Set&lt;Long&gt; roleSet = userCache.getRoleSet(uid);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> isAdmin(roleSet) || roleSet.contains(roleEnum.getId());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">isAdmin</span><span class="params">(Set&lt;Long&gt; roleSet)</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> roleSet.contains(RoleEnum.ADMIN.getId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>将用户写入黑名单，并且通过SpringEvent发送异步事件处理黑名单事件</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="meta">@Transactional(rollbackFor = Exception.class)</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">black</span><span class="params">(BlackReq blackReq)</span> &#123;</span><br><span class="line">     <span class="comment">// 将id写入黑名单</span></span><br><span class="line">     <span class="type">Long</span> <span class="variable">uid</span> <span class="operator">=</span> blackReq.getUid();</span><br><span class="line">     <span class="type">Black</span> <span class="variable">black</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Black</span>();</span><br><span class="line">     black.setType(BlackTypeEnum.UID.getId());</span><br><span class="line">     black.setTarget(uid.toString());</span><br><span class="line">     blackDao.save(black);</span><br><span class="line">    <span class="comment">// 将ip写入黑名单</span></span><br><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userDao.getById(uid);</span><br><span class="line">    blackIp(Optional.ofNullable(user.getIpInfo().getCreateIp()).orElse(<span class="literal">null</span>));</span><br><span class="line">    blackIp(Optional.ofNullable(user.getIpInfo().getUpdateIp()).orElse(<span class="literal">null</span>));</span><br><span class="line"></span><br><span class="line">    applicationEventPublisher.publishEvent(<span class="keyword">new</span> <span class="title class_">UserBlackEvent</span>(<span class="built_in">this</span>, user));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 封禁IP</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> ip</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">blackIp</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (StrUtil.isBlank(ip)) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">Black</span> <span class="variable">insert</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Black</span>();</span><br><span class="line">        insert.setTarget(ip);</span><br><span class="line">        insert.setType(BlackTypeEnum.IP.getId());</span><br><span class="line">        blackDao.save(insert);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>监听黑名单事件并作处理【推送前端消息，更新用户状态，清空黑名单缓存列表(拦截器处理的时候会用到)】</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserBlackListener</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> WebSocketService webSocketService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserDao userDao;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserCache userCache;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 推送前端消息</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> event</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Async</span> <span class="comment">// 异步执行</span></span><br><span class="line">    <span class="meta">@TransactionalEventListener(classes = UserBlackEvent.class, phase = TransactionPhase.AFTER_COMMIT)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendMsg</span><span class="params">(UserBlackEvent event)</span>&#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> event.getUser();</span><br><span class="line">        webSocketService.sendMsgToAll(WebSocketAdapter.buildBlack(user));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 更新用户状态</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> event</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Async</span> <span class="comment">// 异步执行</span></span><br><span class="line">    <span class="meta">@TransactionalEventListener(classes = UserBlackEvent.class, phase = TransactionPhase.AFTER_COMMIT)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">changeActivity</span><span class="params">(UserBlackEvent event)</span>&#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> event.getUser();</span><br><span class="line">        userDao.invalidUid(user.getId());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 清空黑名单缓存</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> event</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Async</span> <span class="comment">// 异步执行</span></span><br><span class="line">    <span class="meta">@TransactionalEventListener(classes = UserBlackEvent.class, phase = TransactionPhase.AFTER_COMMIT)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">clearBlackList</span><span class="params">(UserBlackEvent event)</span>&#123;</span><br><span class="line">        userCache.evictBlack();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>推送给前端所有用户这里用到了线程池，加快处理效率</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> ThreadPoolTaskExecutor threadPoolTaskExecutor;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendMsgToAll</span><span class="params">(WSBaseResp&lt;?&gt; resp)</span> &#123;</span><br><span class="line">    ONLINE_WS_MAP.forEach(((channel, wsChannelExtraDTO) -&gt; &#123;</span><br><span class="line">        <span class="comment">// 使用线程池，加快发送速度</span></span><br><span class="line">        threadPoolTaskExecutor.execute(() -&gt; &#123;</span><br><span class="line">            sendMsg(channel, resp);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="游标翻页"><a href="#游标翻页" class="headerlink" title="游标翻页"></a>游标翻页</h2><p>游标翻页更适用于一页一页跳转的场景，而对于跳页反转（例如第一页跳到第五页就不适用）</p>
<p>本项目为聊天项目，适合适用游标翻页</p>
<h3 id="深翻页问题"><a href="#深翻页问题" class="headerlink" title="深翻页问题"></a>深翻页问题</h3><p>普通翻页前端一般会有个分页条。能够指定一页的<strong>条数</strong>，以及任意选择查看<strong>第几页</strong>。</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1685187350715-ec688988-ee5a-426e-88ea-dbb85f6aac22.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_16,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>对应的参数就是<code>pageNo</code>和<code>pageSize</code></p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1685187427310-3e189d14-a215-4dbf-b86d-5badf5bd5a12.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_14,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>假设前端想要查看第11页的内容，传的值<code>pageNo</code>&#x3D;11，<code>pageSize</code>&#x3D;10</p>
<p>其中<code>100</code>代表需要跳过的条数，<code>10</code>代表跳过指定条数后，往后需要再取的条数。</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1685188112380-ab4f8dec-f339-451b-87eb-2ae6ae1bfbe1.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_12,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>对应图片就是这样的一个效果，需要在数据库的位置先读出100条，然后<strong>丢弃</strong>。丢弃完100条后，再继续取10条<strong>选用</strong>。</p>
<p>如果我们翻页到了很深的地方，比如读到了第1000页，对应的sql语句就是<code>select * from table limit 10000,10</code></p>
<p>需要先查询10000条进行丢弃，再取那么个10条选用。这个效率也太低了，</p>
<blockquote>
<p> 我们经常需要定时任务全量去跑一张表的数据，普通翻页去跑的话，到后面数据量大的时候，就会越跑越慢，这就是深翻页带来的问题。</p>
</blockquote>
<p><strong>解决办法</strong></p>
<p>目前的问题在于每次翻页都需要花时间扫描一些不需要的记录，然后丢弃。那么是不是可以优化这个步骤呢？</p>
<p>以后不论第几页，我们都不需要跳过一些值。直接取<code>limit 0,10</code>。这样语句变成了<code>select * from table limit 0,10</code></p>
<p>取到的是1-10这些记录，取不到我们想要的101-110，没关系，再加一个条件<code>select * from table where id&gt;100 order by id limit 0,10</code></p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1685188635346-68f634f3-3276-4f87-8788-4691ec990d83.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_13,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>只要id这个字段有索引，就能直接定位到101这个字段，然后去10条记录。以后无论翻页到多大，通过索引直接定位到读取的位置，效率基本是一样的。这个<code>id&gt;100</code>就是我们的游标，这就是<strong>游标翻页。</strong></p>
<h3 id="游标介绍"><a href="#游标介绍" class="headerlink" title="游标介绍"></a>游标介绍</h3><p>游标翻页可以完美的解决深翻页问题，依赖的就是我们的游标，即<code>cursor</code>。针对mysql的游标翻页，我们需要通过<code>cursor</code>快速定位到指定记录，意味着游标必须添加索引。</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1685189222316-cf64fcf1-da85-46f0-b6b0-4088cd66b89e.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_15,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>前端之前传的<code>pageNo</code>字段改成了<code>cursor</code>字段。<code>cursor</code>是上一次查询结果的位置，作为下一次查询的游标，由后端返回</p>
<p>我们来模拟一次前后端的翻页交互</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1685189899438-884c817d-0ee5-4b7d-a540-6be3bb135975.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_19,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>随着翻页的持续，游标不断往翻页的方向推进。</p>
<p><strong>所以游标翻页不适合跳页，只能不断的往下翻。更适合C端的列表场景</strong></p>
<h3 id="总结-3"><a href="#总结-3" class="headerlink" title="总结"></a>总结</h3><p>游标翻页的<code>优点</code>：</p>
<p>1.解决深翻页问题</p>
<p>2.解决频繁变动的列表翻页问题。</p>
<p><code>缺点</code>：</p>
<p>1.无法跳页，只能不断往下翻</p>
<p>游标翻页更适合c端场景，用户只能不断下滑翻页。</p>
<p>普通翻页更适合B端场景。用户能看见总页数，能随意跳页</p>
<h2 id="前后端资源懒加载"><a href="#前后端资源懒加载" class="headerlink" title="前后端资源懒加载"></a>前后端资源懒加载</h2><h3 id="选用原因"><a href="#选用原因" class="headerlink" title="选用原因"></a>选用原因</h3><h4 id="带宽占用"><a href="#带宽占用" class="headerlink" title="带宽占用"></a>带宽占用</h4><p>对于项目中的个别请求，有时候会有大量冗余和重复的消息。特别是用户的<code>头像</code>，<code>名称</code>，<code>佩戴徽章</code>，这些东西，基本都是重复的，但是我们每一次的消息推送，成员上下线，都需要携带。</p>
<p>对于这些数据，其实我们只想知道消息的拥有者是谁，也就是uid。其他的头像，徽章，这些我们之前都有，但是他们却占据了消息体的大部分内容，有效载荷太少。</p>
<p>在压测的时候发现了很多时候我们的<strong>瓶颈就在带宽</strong>上。而带宽都被头像的url给占用了。</p>
<h4 id="代码复杂度"><a href="#代码复杂度" class="headerlink" title="代码复杂度"></a>代码复杂度</h4><p>如果每次因为一个简单的消息推送，就要去写一套复杂的代码，去加载用户的名称，以及徽章相关的信息。都会增大后端代码的复杂度。</p>
<p>每次组装vo数据，都得去查出用户的关联信息，进行组装。哪怕是可以复用这段代码，但是相应的性能损耗，也是不可避免的。</p>
<h3 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h3><h4 id="图片路径协议优化"><a href="#图片路径协议优化" class="headerlink" title="图片路径协议优化"></a>图片路径协议优化</h4><p>前后端可以将用户头像遵循一种固定的路径协议，比如<code>https://img.mallchat.cn/avatar/&#123;uid&#125;.png</code>;</p>
<p>这样只给前端uid，他自己就能组装出用户头像了</p>
<h4 id="懒加载时间点"><a href="#懒加载时间点" class="headerlink" title="懒加载时间点"></a>懒加载时间点</h4><p>前端在什么场景下会请求后端进行信息懒加载，每次请求后端，需要更新自己库里的懒加载时间<code>lastModifyTime</code></p>
<ul>
<li>没数据加载</li>
<li>数据过期加载（校验）：<code>lastModifyTime</code>小于数据库存的<code>lastModifyTime</code></li>
<li>主动加载（校验）：例如用户主动点击头像查询信息</li>
</ul>
<h3 id="总结-4"><a href="#总结-4" class="headerlink" title="总结"></a>总结</h3><p>了解到了项目的<strong>瓶颈</strong>，大多是来源带宽不足。为了<strong>节省带宽</strong>，同时简化后端的逻辑，我们设计了一套<strong>前后端资源懒加载的交互框架</strong>。他有三个懒加载的<strong>时间点</strong>，同时保证了<strong>及时性</strong>，以及<strong>最终一致性</strong>。这个框架的核心就是<strong>lastModifyTime</strong>字段的定义。有了它，只要没有变动，后端都可以不返回用户信息，又能节省一大部分带宽。并且为了不需要展示的字段不返回null。我们用了一个json的注解，又节省了一小部分带宽。</p>
<h2 id="批量缓存框架"><a href="#批量缓存框架" class="headerlink" title="批量缓存框架"></a>批量缓存框架</h2><p>缓存通常有两种使用方式，一种是Cache-Aside，一种是cache-through。也就是旁路缓存和缓存即数据源。</p>
<p>一般一种用于读，另一种用于读写。</p>
<h3 id="盘路缓存"><a href="#盘路缓存" class="headerlink" title="盘路缓存"></a>盘路缓存</h3><p>我们通常用的都是旁路缓存模式</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1696084655149-bf1c5c2b-3626-45b2-8b43-0d9268f5a6bc.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_14,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>GET缓存获取不到，就LOAD加载数据库，再将数据库写回redis缓存。</p>
<p>盘路缓存也有很好用的框架，比如spring-cache。只需要写load逻辑。加上个注解，就能实现盘路缓存的效果。</p>
<p><strong>缺点</strong>：在极端情况下。我需要获取一批用户的信息，碰巧所有用户的缓存都失效了，都需全部重新加载。那么这样一个本来性能很高的循环，就等同于全部查了数据库，缓存一点儿作用都没了。</p>
<h3 id="批量缓存查询"><a href="#批量缓存查询" class="headerlink" title="批量缓存查询"></a>批量缓存查询</h3><p>对于这种批量查询缓存的需求，传统的旁路缓存框架无法达到我们的需求。我们需要让他能够批量的get or load。</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1696085632392-3b47a5e4-9700-4702-9ede-bef81682e82e.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_12,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>类似这张图，批量的get。发现没有的数据，再批量的load一次，这样和redis以及数据库的交互都只会有一次。</p>
<p><strong>具体代码</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 获取用户信息，盘路缓存模式</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> Map&lt;Long, User&gt; <span class="title function_">getUserInfoBatch</span><span class="params">(Set&lt;Long&gt; uids)</span> &#123;</span><br><span class="line">    <span class="comment">//批量组装key</span></span><br><span class="line">    List&lt;String&gt; keys = uids.stream().map(a -&gt; RedisKey.getKey(RedisKey.USER_INFO_STRING, a)).collect(Collectors.toList());</span><br><span class="line">    <span class="comment">//批量get</span></span><br><span class="line">    List&lt;User&gt; mget = RedisUtils.mget(keys, User.class);</span><br><span class="line">    Map&lt;Long, User&gt; map = mget.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getId, Function.identity()));</span><br><span class="line">    <span class="comment">//发现差集——还需要load更新的uid</span></span><br><span class="line">    List&lt;Long&gt; needLoadUidList = uids.stream().filter(a -&gt; !map.containsKey(a)).collect(Collectors.toList());</span><br><span class="line">    <span class="keyword">if</span> (CollUtil.isNotEmpty(needLoadUidList)) &#123;</span><br><span class="line">        <span class="comment">//批量load</span></span><br><span class="line">        List&lt;User&gt; needLoadUserList = userDao.listByIds(needLoadUidList);</span><br><span class="line">        Map&lt;String, User&gt; redisMap = needLoadUserList.stream().collect(Collectors.toMap(a -&gt; RedisKey.getKey(RedisKey.USER_INFO_STRING, a.getId()), Function.identity()));</span><br><span class="line">        RedisUtils.mset(redisMap, <span class="number">5</span> * <span class="number">60</span>);</span><br><span class="line">        <span class="comment">//加载回redis</span></span><br><span class="line">        map.putAll(needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity())));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> map;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>我们在很多处都要用到这个接口，所以就需要对这个批量查询缓存进行封装</p>
<p>这时候又要轮到我们的常规抽象环节了</p>
<p><img src="" data-original="https://cdn.nlark.com/yuque/0/2023/png/26318626/1696085827579-fa03f689-bdb1-476e-b53f-a291b3314660.png?x-oss-process=image/watermark,type_d3F5LW1pY3JvaGVp,size_34,text_TWFsbENoYXQ=,color_FFFFFF,shadow_50,t_80,g_se,x_10,y_10" alt="img"></p>
<p>黄色代表可复用的流程，红色代表个性化的代码。</p>
<h3 id="批量缓存框架-1"><a href="#批量缓存框架-1" class="headerlink" title="批量缓存框架"></a>批量缓存框架</h3><p>先创建批量缓存框架的接口</p>
<p>接口</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">BatchCache</span>&lt;IN, OUT&gt; &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取单个</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    OUT <span class="title function_">get</span><span class="params">(IN req)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取批量</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    Map&lt;IN, OUT&gt; <span class="title function_">getBatch</span><span class="params">(List&lt;IN&gt; req)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改删除单个</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">delete</span><span class="params">(IN req)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 修改删除多个</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">deleteBatch</span><span class="params">(List&lt;IN&gt; req)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>再通过抽象类，定义好骨架</p>
<p>抽象类</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">AbstractRedisStringCache</span>&lt;IN, OUT&gt; <span class="keyword">implements</span> <span class="title class_">BatchCache</span>&lt;IN, OUT&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Class&lt;OUT&gt; outClass;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="title function_">AbstractRedisStringCache</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">ParameterizedType</span> <span class="variable">genericSuperclass</span> <span class="operator">=</span> (ParameterizedType) <span class="built_in">this</span>.getClass().getGenericSuperclass();</span><br><span class="line">        <span class="built_in">this</span>.outClass = (Class&lt;OUT&gt;) genericSuperclass.getActualTypeArguments()[<span class="number">1</span>];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">abstract</span> String <span class="title function_">getKey</span><span class="params">(IN req)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">abstract</span> Long <span class="title function_">getExpireSeconds</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">abstract</span> Map&lt;IN, OUT&gt; <span class="title function_">load</span><span class="params">(List&lt;IN&gt; req)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> OUT <span class="title function_">get</span><span class="params">(IN req)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> getBatch(Collections.singletonList(req)).get(req);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Map&lt;IN, OUT&gt; <span class="title function_">getBatch</span><span class="params">(List&lt;IN&gt; req)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (CollectionUtil.isEmpty(req)) &#123;<span class="comment">//防御性编程</span></span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//去重</span></span><br><span class="line">        req = req.stream().distinct().collect(Collectors.toList());</span><br><span class="line">        <span class="comment">//组装key</span></span><br><span class="line">        List&lt;String&gt; keys = req.stream().map(<span class="built_in">this</span>::getKey).collect(Collectors.toList());</span><br><span class="line">        <span class="comment">//批量get</span></span><br><span class="line">        List&lt;OUT&gt; valueList = RedisUtils.mget(keys, outClass);</span><br><span class="line">        <span class="comment">//差集计算</span></span><br><span class="line">        List&lt;IN&gt; loadReqs = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; valueList.size(); i++) &#123;</span><br><span class="line">            <span class="keyword">if</span> (Objects.isNull(valueList.get(i))) &#123;</span><br><span class="line">                loadReqs.add(req.get(i));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        Map&lt;IN, OUT&gt; load = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        <span class="comment">//不足的重新加载进redis</span></span><br><span class="line">        <span class="keyword">if</span> (CollectionUtil.isNotEmpty(loadReqs)) &#123;</span><br><span class="line">            <span class="comment">//批量load</span></span><br><span class="line">            load = load(loadReqs);</span><br><span class="line">            Map&lt;String, OUT&gt; loadMap = load.entrySet().stream()</span><br><span class="line">                    .map(a -&gt; Pair.of(getKey(a.getKey()), a.getValue()))</span><br><span class="line">                    .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));</span><br><span class="line">            RedisUtils.mset(loadMap, getExpireSeconds());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//组装最后的结果</span></span><br><span class="line">        Map&lt;IN, OUT&gt; resultMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; req.size(); i++) &#123;</span><br><span class="line">            <span class="type">IN</span> <span class="variable">in</span> <span class="operator">=</span> req.get(i);</span><br><span class="line">            <span class="type">OUT</span> <span class="variable">out</span> <span class="operator">=</span> Optional.ofNullable(valueList.get(i))</span><br><span class="line">                    .orElse(load.get(in));</span><br><span class="line">            resultMap.put(in, out);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> resultMap;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">delete</span><span class="params">(IN req)</span> &#123;</span><br><span class="line">        deleteBatch(Collections.singletonList(req));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deleteBatch</span><span class="params">(List&lt;IN&gt; req)</span> &#123;</span><br><span class="line">        List&lt;String&gt; keys = req.stream().map(<span class="built_in">this</span>::getKey).collect(Collectors.toList());</span><br><span class="line">        RedisUtils.del(keys);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这时候你就会发现，写一个批量的旁路缓存有多简单。</p>
<p>具体实现</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserInfoCache</span> <span class="keyword">extends</span> <span class="title class_">AbstractRedisStringCache</span>&lt;Long, User&gt; &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserDao userDao;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> String <span class="title function_">getKey</span><span class="params">(Long uid)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> RedisKey.getKey(RedisKey.USER_INFO_STRING, uid);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> Long <span class="title function_">getExpireSeconds</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">5</span> * <span class="number">60L</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> Map&lt;Long, User&gt; <span class="title function_">load</span><span class="params">(List&lt;Long&gt; uidList)</span> &#123;</span><br><span class="line">        List&lt;User&gt; needLoadUserList = userDao.listByIds(uidList);</span><br><span class="line">        <span class="keyword">return</span> needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta"><i class="fas fa-circle-user fa-fw"></i>文章作者: </span><span class="post-copyright-info"><a href="http://www.lijunxi.site">Jixer</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta"><i class="fas fa-square-arrow-up-right fa-fw"></i>文章链接: </span><span class="post-copyright-info"><a href="http://www.lijunxi.site/posts/2967897711/">http://www.lijunxi.site/posts/2967897711/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta"><i class="fas fa-circle-exclamation fa-fw"></i>版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来自 <a href="http://www.lijunxi.site" target="_blank">Jixer的小屋</a>！</span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/SpringBoot/">SpringBoot</a></div><div class="post_share"><div class="social-share" data-image="https://q1.qlogo.cn/g?b=qq&amp;nk=2770063826&amp;s=640" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="/pluginsSrc/butterfly-extsrc/sharejs/dist/css/share.min.css?v=1.1.3" media="print" onload="this.media='all'"><script src="/pluginsSrc/butterfly-extsrc/sharejs/dist/js/social-share.min.js?v=1.1.3" defer></script></div></div><nav class="pagination-post" id="pagination"><div class="prev-post pull-left"><a href="/posts/3305483931/" title="Java面试题基础(上)"><div class="cover" style="background: var(--default-bg-color)"></div><div class="pagination-info"><div class="label">上一篇</div><div class="prev_info">Java面试题基础(上)</div></div></a></div><div class="next-post pull-right"><a href="/posts/3560535565/" title="RabbitMQ学习笔记"><div class="cover" style="background: var(--default-bg-color)"></div><div class="pagination-info"><div class="label">下一篇</div><div class="next_info">RabbitMQ学习笔记</div></div></a></div></nav><div class="relatedPosts"><div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>相关推荐</span></div><div class="relatedPosts-list"><div><a href="/posts/4283085990/" title="SpringBoot整合Minio"><div class="cover" style="background: var(--default-bg-color)"></div><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2024-01-26</div><div class="title">SpringBoot整合Minio</div></div></a></div><div><a href="/posts/175653331/" title="SpringBoot整合支付宝支付"><div class="cover" style="background: var(--default-bg-color)"></div><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2024-01-27</div><div class="title">SpringBoot整合支付宝支付</div></div></a></div><div><a href="/posts/2769134519/" title="黑马点评项目笔记"><div class="cover" style="background: var(--default-bg-color)"></div><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2024-01-26</div><div class="title">黑马点评项目笔记</div></div></a></div></div></div></div><div class="aside-content" id="aside-content"><div class="card-widget card-info"><div class="is-center"><div class="avatar-img"><img src="" data-original="https://q1.qlogo.cn/g?b=qq&amp;nk=2770063826&amp;s=640" onerror="this.onerror=null;this.src='/img/friend_404.gif'" alt="avatar"/></div><div class="author-info__name">Jixer</div><div class="author-info__description"></div></div><div class="card-info-data site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">52</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">19</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">7</div></a></div><a id="card-info-btn" target="_blank" rel="noopener" href="https://github.com/2770063826"><i class="fab fa-github"></i><span>Follow Me</span></a></div><div class="card-widget card-announcement"><div class="item-headline"><i class="fas fa-bullhorn fa-shake"></i><span>公告</span></div><div class="announcement_content"></div></div><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>目录</span><span class="toc-percentage"></span></div><div class="toc-content"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%BE%AE%E4%BF%A1%E6%89%AB%E7%A0%81%E7%99%BB%E5%BD%95"><span class="toc-text">微信扫码登录</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%87%86%E5%A4%87"><span class="toc-text">准备</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E5%B7%A5%E5%85%B7"><span class="toc-text">工具</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E5%BC%95%E5%85%A5%E4%BE%9D%E8%B5%96"><span class="toc-text">引入依赖</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6"><span class="toc-text">配置文件</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E9%85%8D%E7%BD%AE%E6%B6%88%E6%81%AF%E5%A4%84%E7%90%86%E5%99%A8"><span class="toc-text">配置消息处理器</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B"><span class="toc-text">执行过程</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#1%E3%80%81%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E4%BA%8C%E7%BB%B4%E7%A0%81"><span class="toc-text">1、获取登录二维码</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#2%E3%80%81%E5%85%AC%E4%BC%97%E5%8F%B7%E8%BF%94%E5%9B%9E%E7%99%BB%E5%BD%95%E6%8E%88%E6%9D%83"><span class="toc-text">2、公众号返回登录授权</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#3%E3%80%81%E5%BE%AE%E4%BF%A1%E8%B0%83%E7%94%A8%E7%BB%99%E5%AE%9A%E7%9A%84%E5%9B%9E%E8%B0%83%E6%8E%A5%E5%8F%A3"><span class="toc-text">3、微信调用给定的回调接口</span></a></li></ol></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E7%94%A8%E6%88%B7%E8%AE%A4%E8%AF%81%E6%8A%80%E6%9C%AFToken%E6%96%B9%E6%A1%88"><span class="toc-text">用户认证技术Token方案</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#Cookie-Session-%E7%99%BB%E5%BD%95"><span class="toc-text">Cookie + Session 登录</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#Cookie%E7%9A%84%E8%AE%BE%E7%BD%AE%E5%8E%9F%E7%90%86"><span class="toc-text">Cookie的设置原理</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%BC%BA%E7%82%B9"><span class="toc-text">缺点</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%80%BB%E7%BB%93"><span class="toc-text">总结</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#JWT%E5%AE%9E%E7%8E%B0token"><span class="toc-text">JWT实现token</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E4%BC%98%E7%82%B9"><span class="toc-text">优点</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%BC%BA%E7%82%B9-1"><span class="toc-text">缺点</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%80%BB%E7%BB%93-1"><span class="toc-text">总结</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%8F%8Ctoken%E6%96%B9%E6%A1%88"><span class="toc-text">双token方案</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%80%BB%E7%BB%93-2"><span class="toc-text">总结</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%B8%AD%E5%BF%83%E7%AE%A1%E7%90%86token"><span class="toc-text">中心管理token</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">分布式锁</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%BC%96%E7%A8%8B%E5%BC%8F"><span class="toc-text">编程式</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%B3%A8%E8%A7%A3"><span class="toc-text">注解</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#IP%E5%BD%92%E5%B1%9E%E5%9C%B0"><span class="toc-text">IP归属地</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%85%B7%E4%BD%93%E6%B5%81%E7%A8%8B"><span class="toc-text">具体流程</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81"><span class="toc-text">核心代码</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E9%BB%91%E5%90%8D%E5%8D%95%E5%8A%9F%E8%83%BD"><span class="toc-text">黑名单功能</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%85%B7%E4%BD%93%E6%B5%81%E7%A8%8B-1"><span class="toc-text">具体流程</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81-1"><span class="toc-text">核心代码</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%B8%B8%E6%A0%87%E7%BF%BB%E9%A1%B5"><span class="toc-text">游标翻页</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%B7%B1%E7%BF%BB%E9%A1%B5%E9%97%AE%E9%A2%98"><span class="toc-text">深翻页问题</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%B8%B8%E6%A0%87%E4%BB%8B%E7%BB%8D"><span class="toc-text">游标介绍</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%80%BB%E7%BB%93-3"><span class="toc-text">总结</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%89%8D%E5%90%8E%E7%AB%AF%E8%B5%84%E6%BA%90%E6%87%92%E5%8A%A0%E8%BD%BD"><span class="toc-text">前后端资源懒加载</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%80%89%E7%94%A8%E5%8E%9F%E5%9B%A0"><span class="toc-text">选用原因</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E5%B8%A6%E5%AE%BD%E5%8D%A0%E7%94%A8"><span class="toc-text">带宽占用</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E4%BB%A3%E7%A0%81%E5%A4%8D%E6%9D%82%E5%BA%A6"><span class="toc-text">代码复杂度</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95"><span class="toc-text">解决办法</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E5%9B%BE%E7%89%87%E8%B7%AF%E5%BE%84%E5%8D%8F%E8%AE%AE%E4%BC%98%E5%8C%96"><span class="toc-text">图片路径协议优化</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%87%92%E5%8A%A0%E8%BD%BD%E6%97%B6%E9%97%B4%E7%82%B9"><span class="toc-text">懒加载时间点</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%80%BB%E7%BB%93-4"><span class="toc-text">总结</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%89%B9%E9%87%8F%E7%BC%93%E5%AD%98%E6%A1%86%E6%9E%B6"><span class="toc-text">批量缓存框架</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%9B%98%E8%B7%AF%E7%BC%93%E5%AD%98"><span class="toc-text">盘路缓存</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%89%B9%E9%87%8F%E7%BC%93%E5%AD%98%E6%9F%A5%E8%AF%A2"><span class="toc-text">批量缓存查询</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%89%B9%E9%87%8F%E7%BC%93%E5%AD%98%E6%A1%86%E6%9E%B6-1"><span class="toc-text">批量缓存框架</span></a></li></ol></li></ol></div></div><div class="card-widget card-recent-post"><div class="item-headline"><i class="fas fa-history"></i><span>最新文章</span></div><div class="aside-list"><div class="aside-list-item no-cover"><div class="content"><a class="title" href="/posts/2029624507/" title="2022年算法队选拔赛">2022年算法队选拔赛</a><time datetime="2024-05-09T15:00:27.000Z" title="发表于 2024-05-09 23:00:27">2024-05-09</time></div></div><div class="aside-list-item no-cover"><div class="content"><a class="title" href="/posts/1978524057/" title="牛客小白月赛84">牛客小白月赛84</a><time datetime="2024-05-08T14:40:35.000Z" title="发表于 2024-05-08 22:40:35">2024-05-08</time></div></div><div class="aside-list-item no-cover"><div class="content"><a class="title" href="/posts/131339317/" title="软件测试资料">软件测试资料</a><time datetime="2024-05-07T03:12:52.000Z" title="发表于 2024-05-07 11:12:52">2024-05-07</time></div></div><div class="aside-list-item no-cover"><div class="content"><a class="title" href="/posts/2394234105/" title="第十四届蓝桥杯B组国赛">第十四届蓝桥杯B组国赛</a><time datetime="2024-05-05T13:40:15.000Z" title="发表于 2024-05-05 21:40:15">2024-05-05</time></div></div><div class="aside-list-item no-cover"><div class="content"><a class="title" href="/posts/1405472621/" title="Leetcode第396场周赛">Leetcode第396场周赛</a><time datetime="2024-05-05T03:58:25.000Z" title="发表于 2024-05-05 11:58:25">2024-05-05</time></div></div></div></div></div></div></main><footer id="footer"><div id="footer-wrap"><div class="copyright">&copy;2024 By Jixer</div><div class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div><div class="footer_custom_text"><a href="https://beian.miit.gov.cn/#/Integrated/index" style="color:white" target="_blank">蜀ICP备2022009955号-1</a></div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="阅读模式"><i class="fas fa-book-open"></i></button><button id="darkmode" type="button" title="浅色和深色模式转换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside-config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="目录"><i class="fas fa-list-ul"></i></button><button id="go-up" type="button" title="回到顶部"><span class="scroll-percent"></span><i class="fas fa-arrow-up"></i></button></div></div><div><script src="/js/utils.js?v=4.13.0"></script><script src="/js/main.js?v=4.13.0"></script><script src="/pluginsSrc/@fancyapps/ui/dist/fancybox/fancybox.umd.js?v=5.0.33"></script><div class="js-pjax"></div><script src="/js/custom-fancybox-umd-min.js"></script><script src="/js/custom-busuanzi-pure-mini.js"></script><script src="/js/Valine.min.js"></script><script src="/js/custom-social-share.min.js"></script><script src="/js/custom-typed-umd-min.js"></script><script src="/js/av-min.js"></script><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script><div id="local-search"><div class="search-dialog"><nav class="search-nav"><span class="search-dialog-title">搜索</span><span id="loading-status"></span><button class="search-close-button"><i class="fas fa-times"></i></button></nav><div class="is-center" id="loading-database"><i class="fas fa-spinner fa-pulse"></i><span>  数据库加载中</span></div><div class="search-wrap"><div id="local-search-input"><div class="local-search-box"><input class="local-search-box--input" placeholder="搜索文章" type="text"/></div></div><hr/><div id="local-search-results"></div><div id="local-search-stats-wrap"></div></div></div><div id="search-mask"></div><script src="/js/search/local-search.js?v=4.13.0"></script></div></div>
        <style>
            [bg-lazy] {
                background-image: none !important;
                background-color: #eee !important;
            }
        </style>
        <script>
            window.imageLazyLoadSetting = {
                isSPA: false,
                preloadRatio: 1,
                processImages: null,
            };
        </script><script>window.addEventListener("load",function(){var t=/\.(gif|jpg|jpeg|tiff|png)$/i,r=/^data:image\/[a-z]+;base64,/;Array.prototype.slice.call(document.querySelectorAll("img[data-original]")).forEach(function(a){var e=a.parentNode;"A"===e.tagName&&(e.href.match(t)||e.href.match(r))&&(e.href=a.dataset.original)})});</script><script>!function(r){r.imageLazyLoadSetting.processImages=t;var e=r.imageLazyLoadSetting.isSPA,n=r.imageLazyLoadSetting.preloadRatio||1,c=a();function a(){var t=Array.prototype.slice.call(document.querySelectorAll("img[data-original]")),e=Array.prototype.slice.call(document.querySelectorAll("[bg-lazy]"));return t.concat(e)}function t(){e&&(c=a());for(var t,o=0;o<c.length;o++)0<=(t=(t=c[o]).getBoundingClientRect()).bottom&&0<=t.left&&t.top<=(r.innerHeight*n||document.documentElement.clientHeight*n)&&function(){var t,e,n,a,i=c[o];e=function(){c=c.filter(function(t){return i!==t}),r.imageLazyLoadSetting.onImageLoaded&&r.imageLazyLoadSetting.onImageLoaded(i)},(t=i).hasAttribute("bg-lazy")?(t.removeAttribute("bg-lazy"),e&&e()):(n=new Image,a=t.getAttribute("data-original"),n.onload=function(){t.src=a,t.removeAttribute("data-original"),e&&e()},t.src!==a&&(n.src=a))}()}function i(){clearTimeout(t.tId),t.tId=setTimeout(t,500)}t(),document.addEventListener("scroll",i),r.addEventListener("resize",i),r.addEventListener("orientationchange",i)}(this);</script></body></html>